package phases

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path"
	fp "path/filepath"
	"strings"
	"sync"
	"time"

	ckeapi "cke.io/ckectl/app/apis"
	"cke.io/ckectl/app/constants"
	util "cke.io/ckectl/app/util"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"k8s.io/apimachinery/pkg/util/json"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow"
	kubeconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
	utilsexec "k8s.io/utils/exec"
)

// get Config form init
func GetInitConfiguration(c workflow.RunData) (cfg *ckeapi.CkeConfiguration, err error) {
	data, ok := c.(PrepareData)
	if !ok {
		return nil, errors.New("etcd phase invoked with an invalid data struct")
	}

	return data.Cfg(), err
}

// get common write
func getCommonWriter(c workflow.RunData) io.Writer{
	w, ok := c.(CommonWriter)
	if ok {
		return w.OutputWriter()
	}

	writer, err := os.OpenFile(constants.AnsibleLogFile, os.O_CREATE|os.O_RDWR, 0600)
	if err!=nil {
		fmt.Println("err when open log file")
		return os.Stdout
	}

	return writer
}

func WriteAnsibleLogf(file string, format string, a ...interface{}) {
	var date = time.Now().Format(constants.RFC3339AtNoZ)
	var filepath = fmt.Sprintf("%s/%s.%s.log", constants.AnsibleLogDir, date, file)
	writeLogFile(filepath, format, a...)
}

func FlashAndSetDoneFile(done string) {
	writeLogFile(done, "")
	writeLogFile(done + ".debug", "")
	constants.AnsibleDoneFile = done
}

func PhaseDoneOrFailure(phase string, err error) {
	phaseDoneOrFailure(constants.AnsibleDoneFile, phase, err)
}

func phaseDoneOrFailure(done, phase string, err error) {
	writer, errs := os.OpenFile(fp.Clean(done), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
	if errs != nil {
		fmt.Println(errs)
		return
	}

	defer func() {
		if err := writer.Close(); err != nil {
			logrus.Errorf("Error when closing file, %s\n", err)
		}
	}()

	// it means all phase done, so write done massage
	if err==nil && phase=="" {
		_, _ = writer.WriteString("Done\n")
		return
	}

	if err==nil {
		_, _ = writer.WriteString(phase + " OK\n")
	} else{
		_, _ = writer.WriteString(phase + " Fail. " + strings.ReplaceAll(err.Error(), "\n", "\\n") + "\n")
	}
}

func WritePhaseDoneMsg(cmd, phase string, cfg *ckeapi.CkeConfiguration, err error) error {
	var writeError error = nil
	if err != nil {
		writeError = errors.New(GetAnsibleErrorMessage(err))
	}

	if cmd != "" { cmd = cmd + " cmd subphase "}
	PhaseDoneOrFailure(cmd + phase, writeError)

	PostInformationToGenssisNodes(constants.AnsibleDoneFile, cfg.Ansible.CkeadmWaitAddress, cfg.Ansible.ClusterID, cfg.Ansible.LocalHostName, constants.AnsibleDoneFile+".debug")
	return writeError
}

func PostInformationToGenssisNodes(doneFile, requestAddress, clusterId, nodeName string, logFile string) {
	var DoneContext string
	var writer io.Writer = bytes.NewBuffer([]byte{})

	if file, err := os.OpenFile(fp.Clean(logFile), os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600); err == nil {
		writer = file

		defer func() {
			if err := file.Close(); err != nil {
				logrus.Errorf("Error when closing file, %s\n", err)
			}
		}()
	}

	fmt.Fprintf(writer, "===> request has been start\n")
	fmt.Fprintf(writer, "debug: DoneFile=%s, RequestAddress=%s, ClusterID=%s, NodeName=%s, LogWriter=%s\n", doneFile, requestAddress, clusterId, nodeName, logFile)

	if buff, err := ioutil.ReadFile(fp.Clean(doneFile)); err == nil {
		DoneContext = string(buff)
	}
	fmt.Fprintf(writer, "get from done file, context: \n%s\n", DoneContext)

	body, err := json.Marshal(ckeapi.LogContext {
		ClusterID:   clusterId,
		NodeName:    nodeName,
		DoneFile:    path.Base(doneFile), // just file name
		DoneInfo:    DoneContext,
	})
	fmt.Fprintf(writer, "get post json file, context: \n%s\n", string(body))

	if err != nil {
		fmt.Fprintf(writer, "while marshal json error: %s\n", err.Error())
	}

	httpClient := & http.Client {
		Timeout:       5*time.Second,
		Transport:     & http.Transport {
			DisableKeepAlives:   true,
			ResponseHeaderTimeout: 3 * time.Second,
		},
	}

	for RetryTime := 5; RetryTime > 0; RetryTime-- {
		fmt.Fprintf(writer, "trying %d to post info to %s\n", RetryTime, requestAddress)

		_, postErr := httpClient.Post(requestAddress, "application/json", strings.NewReader(string(body)))
		fmt.Fprintf(writer, "post information and get err is: %v\n", postErr)

		if postErr == nil {
			break
		}
		time.Sleep(1*time.Second)
	}

	fmt.Fprintf(writer, "end to send information to genssis nodes\n\n\n\n")
}

func writeLogFile(file string, format string, a ...interface{}) {
	err := os.Mkdir(constants.AnsibleLogDir, os.ModePerm)

	writer, err := os.OpenFile(fp.Clean(file), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
	if err!=nil {
		fmt.Println(err)
		return
	}

	defer func() {
		if err := writer.Close(); err != nil {
			logrus.Errorf("Error when closing file, %s\n", err)
		}
	}()

	fmt.Fprintf(writer, format, a...)
}

func CheckInsecureRegistryConfiged(InsecureRegistry string) bool {
	var format = fmt.Sprintf(`{{$registry := index .RegistryConfig.IndexConfigs "%s"}}{{if $registry}}{{$registry.Secure}}{{end}}`, InsecureRegistry)

	// #nosec
	var out, err = exec.Command("docker", "info", "-f", format).CombinedOutput()
	return err == nil && string(out) == fmt.Sprintf("%t\n", false)
}

func WriterIncertRegistry(InsecureRegistry []string) error {
	if _, err := os.Stat(constants.UserDockerServiceFile); err != nil {
		_ = os.Rename(constants.DockerServiceFile, constants.UserDockerServiceFile)
	}

	var dockerarguments string
	for _, registry := range InsecureRegistry {
		if !CheckInsecureRegistryConfiged(registry) {
			dockerarguments = fmt.Sprintf("%s --insecure-registry=%s", dockerarguments, registry)
		}
	}

	if len(dockerarguments) != 0 {
		// #nosec
		if err := exec.Command("sed", "-i", fmt.Sprintf("s/%s/%s%s/g", "bin\\/dockerd", "bin\\/dockerd", dockerarguments), constants.UserDockerServiceFile).Run(); err != nil {
			fmt.Println("fail to write insecure registry, err is", err.Error())
		}

		return DockerRestart()
	}

	return nil
}

func RemoveIncertRegistry() error {
	err := os.Remove(constants.DockerConfigDir + "/" + "kubeadm-docker.conf")
	if err != nil {
		fmt.Println(err)
	}

	return DockerRestart()
}

func HandleError(expr ...interface{}) error{
	if len(expr)==0 {
		return nil
	}

	err, ok := expr[len(expr)-1].(error)
	if ok {
		return err
	}

	return nil
}

func DockerRestart() error {
	runtime, err := util.NewContainerRuntime(utilsexec.New(), kubeconstants.DefaultDockerCRISocket)
	utilruntime.HandleError(err)

	err = utilsexec.New().Command("systemctl", "daemon-reload").Run()
	err = utilsexec.New().Command("systemctl", "restart", "containerd").Run()
	err = utilsexec.New().Command("systemctl", "restart", "docker").Run()

	return WaitForContainerRuntime(runtime)
}

func WaitForContainerRuntime(runtime util.ContainerInterface) error {
	// wait for docker start util restart timeout
	return wait.PollImmediate(constants.DockerCallRetryInterval, constants.DockerRestartTimeout, func() (bool, error) {
		err := runtime.IsRunning()
		if err != nil {
			return false, nil
		}

		return true, nil
	})
}

// run ansible, level = 1, 2, or 3
func AnsibleRunner(cfg *ckeapi.CkeConfiguration, invFile, ymlFile string, logLevel int, writer io.Writer) error {
	img, level := constants.GetAnsibleImage(cfg), ""

	dockerRunTime, err := util.NewContainerRuntime(utilsexec.New(), kubeconstants.DefaultDockerCRISocket)
	utilruntime.HandleError(err)

	if exist, err := dockerRunTime.ImageExists(img); err!=nil || !exist {
		if err = wait.PollImmediate(constants.PullImageInterval, constants.PullImageTimeout, pullImageCondition(dockerRunTime, img)); err != nil {
			return fmt.Errorf("timeout after %dmin pull image %s", constants.PullImageTimeout/time.Minute, img)
		}
	}

	switch logLevel {
	case 1: level = "-v"
	case 2: level = "-vv"
	case 3: level = "-vvv"
	}

	var externalArgs = fmt.Sprintf("-e ARCH=%s", constants.ARCH)
	for item := range cfg.Ansible.ExternalArguments {
		externalArgs = fmt.Sprintf("%s -e %s", externalArgs, cfg.Ansible.ExternalArguments[item])
	}

	var tofile = func(sha256 string, file string) string {
		return fmt.Sprintf("echo %s | base64 -d > %s", sha256, file)
	}

	repCmd := fmt.Sprintf(`sed -i "/hosts: / s/$/:\&%s/" %s`, cfg.Ansible.LocalHostName, ymlFile)

	cmds := []string {
		SetPrivateKeyCommand(cfg.Ansible.SshSecretKey),
		repCmd,
		tofile(cfg.Ansible.NodesConfig, constants.AnsibleConfig),
		tofile(cfg.Ansible.AllConfig, constants.AnsibleAllConfig),
		fmt.Sprintf("cd %s", constants.KubeadmDeployDir),
		fmt.Sprintf("%s -i %s %s -T %s %s %s", constants.AnsiblePlaybookCommand, invFile, ymlFile, constants.AnsibleTimeOut, level, externalArgs),
	}

	err = dockerRunTime.RemoveContainers([]string{constants.DeployContainerName})
	dockerArgs := []string {
		"-i",
		"--network", "host",
		"--name", constants.DeployContainerName,
	}

	if err = dockerRunTime.RunContainer(img, "sh", []string{"-c", ToACommand(cmds)}, cfg.Ansible.Mounts, dockerArgs, writer); err != nil {
		return errors.New("error while run command: " + err.Error())
	}
	return nil
}

func pullImageCondition(runtime util.ContainerInterface, imgname string) func() (bool, error) {
	return func() (done bool, err error) {
		logrus.Infof("trying to pull image %s ...", imgname)

		if err = runtime.PullImage(imgname); err != nil {
			logrus.Warningf("fail to pull image, err: %s", err.Error())
		}

		// if success pull docker image, return true and err with nul
		return err == nil, nil
	}
}

func SetPrivateKeyCommand(key string) string {
	return ToACommand([]string{
		fmt.Sprintf("mkdir -p /root/.ssh/"),
		fmt.Sprintf("echo %s | base64 -d > %s", key, constants.RootPrivateKey),
		fmt.Sprintf("chmod 0600 %s", constants.RootPrivateKey),
	})
}

func ToACommand(cmds []string) (cmd string){
	for k, v := range cmds {
		if k+1==len(cmds){
			return cmd + v
		}
		cmd += v + " && "
	}
	return
}

func GetAnsibleErrorMessage(err error) string{
	fatal := strings.Split(err.Error(), "fatal")
	if len(fatal)<=1 || !strings.Contains(fatal[len(fatal)-1], "\"msg\":") {
		return strings.Split(err.Error(), ", output")[0]
	}

	if fatal = strings.Split(strings.Split(fatal[len(fatal)-1], "\"msg\":")[1], "" + "\""); len(fatal)<=1 {
		return strings.Split(err.Error(), ", output")[0]
	}

	return fatal[1]
}

func AddKnowHosts(hosts ...string) error{
	// set max Goroutine to Exec for TimeOut
	maxNum := make(chan int, 20)
	group := sync.WaitGroup{}

	// add len of hosts to wait group
	group.Add(len(hosts))

	for _, host := range hosts {
		maxNum <- 0
		go func(host string) {
			// #nosec
			_ = exec.Command("ssh", "-o", "StrictHostKeyChecking=no", host, "help").Run()
			<- maxNum
			group.Done()
		}(host)
	}

	group.Wait()
	return nil
}

func AddKnowDomains(domains ...string) error {
	if len(domains) == 0 {
		return nil
	}
	maxNum := make(chan int, 20)
	group := sync.WaitGroup{}

	// add len of hosts to wait group
	group.Add(len(domains))

	for _, ip_domain := range domains {
		maxNum <- 0
		go func(value string) {
			// fomat 192.168.1.3/registry.docker.com
			if strings.Count(value, "/") == 1 {
				ip := strings.Split(value, "/")[0]
				domain := strings.Split(value, "/")[1]
				// #nosec
				_ = exec.Command("sh", "-c", fmt.Sprintf(`sed -i '/%s/d' /etc/hosts && echo '%s %s' >> /etc/hosts`, domain, ip, domain)).Run()
			}
			<- maxNum
			group.Done()
		}(ip_domain)
	}

	group.Wait()
	return nil
}

func SliceContains(sls []string, item string) bool{
	for _, value := range sls {
		if item == value {
			return true
		}
	}
	return false
}

func CommonPhase(cmd, phase string, flags... string) workflow.Phase {
	return workflow.Phase{
		Name:          phase,
		Run:           commonRuner(cmd, phase),
		InheritFlags:  flags,
	}
}

func commonRuner(cmd, phase string) func(runner workflow.RunData) error {
	return func(runner workflow.RunData) error {
		data, err := GetInitConfiguration(runner)
		if err != nil {
			return errors.New("can't get Init Configuration: " + err.Error())
		}

		var filepath = fmt.Sprintf("%s.%s.log", fmt.Sprintf("%s_%s", cmd, phase), time.Now().Format(constants.RFC3339AtNoZ))
		var fileutil = GetFileWriter(constants.AnsibleLogDir, filepath)

		defer func() {
			if err := fileutil.Close(); err != nil {
				logrus.Errorf("Error when closing file, %s\n", err)
			}
		}()

		err = AnsibleRunner(data, constants.AnsibleConfig, fmt.Sprintf("%s/%s/%s.yml", constants.AnsibleYmlDir, cmd, phase), constants.AnsibleLogLevel, fileutil)

		if content, readerr := ioutil.ReadAll(fileutil); err != nil && readerr == nil {
			err = fmt.Errorf("%s, output: %s", err.Error(), content)
		}
		return WritePhaseDoneMsg(cmd, phase, data, err)
	}
}

func GetFileWriter(filedir, filename string) *os.File {
	_ = os.MkdirAll(filedir, os.ModePerm)
	var filepath = fmt.Sprintf("%s/%s", filedir, filename)

	if file, err := os.OpenFile(fp.Clean(filepath), os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600); err == nil {
		return file
	}
	return os.Stdout
}